/* 
 * 	This file is part of VPN Monitor Project 7..

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package monitor.gui;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.EnumMap;

import javax.swing.JPanel;

import monitor.gui.GuiControl.SettingsEnum;

/**
 * Draws a line diagram for every value in Types.
 */
@SuppressWarnings("serial")
public class Diagram extends JPanel implements ColorObserver{
	final static int DIAGRAM_POINTS = 500;
	private static final Point2D.Float margin = new Point2D.Float(60, 30);
	private static final float arrowLength=20, arrowWidth = 10;
	private Arrows arrow = null;
	private DiagramData dd = null;
	private EnumMap<SettingsEnum, Color> colors = null;

	/**
	 * Creates a new instance of Diagram.
	 * @param control Used to register ColorObserver.
	 * @param settings Settings that should be used.
	 */
	Diagram(GuiControl control, EnumMap<SettingsEnum, String>  settings){
		dd = new DiagramData(control, this);
		arrow = new Arrows(this.getWidth(),this.getHeight(), arrowWidth, arrowLength);
		
		this.colors = new EnumMap<SettingsEnum, Color>(SettingsEnum.class);
		this.setBackground(SettingsEnum.color(settings.get(SettingsEnum.COLOR_BACKGROUND)));
		for(SettingsEnum type : SettingsEnum.changeable())
			this.colors.put(type, SettingsEnum.color(settings.get(type)));		
		
		control.addColorObserver(this);
	}
	
	@Override
	public void colorUpdated(SettingsEnum type, Color color) {
		colors.put(type, color);
		if(type == SettingsEnum.COLOR_BACKGROUND)
			this.setBackground(color);
		this.repaint();
	}
	
	@Override
	public void paintComponent(Graphics g){
		super.paintComponent(g);
		Graphics2D g2 = setupGraphics(g);
		g2.setColor(colors.get(SettingsEnum.COLOR_FOREGROUND));
		drawArrows(g2);
		if (dd.dataReceived()) {
			drawScales(g2);
			g2.setColor(colors.get(SettingsEnum.COLOR_DOWNLOAD));
			plot(g2, normalizeData(dd.getDownloadData(), dd.getLargest()));
			g2.setColor(colors.get(SettingsEnum.COLOR_UPLOAD));
			plot(g2, normalizeData(dd.getUploadData(), dd.getLargest()));
			g2.setColor(colors.get(SettingsEnum.COLOR_TOTAL));
			plot(g2, normalizeData(dd.getTotalData(), dd.getLargest()));
		} else
			g2.drawString("No Data", this.getWidth()/2, this.getHeight()/2);
	}
	
	/**
	 * Initiate Graphics2D there antialiasing are set to on,
	 * the basic stroke that are to be used uses CAP_BUTT, JOIN_MITER.
	 * @param g Basic graphic.
	 * @return Graphics2D.
	 */
	private Graphics2D setupGraphics(Graphics g){
		Graphics2D g2 = (Graphics2D)g;
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
		return g2;
	}
	
	/**
	 * Draws a horizontal arrow at the bottom of the panel and
	 * a vertical in the left of the panel.
	 * @param g2 Graphic2D that are to be used for drawing these arrows.
	 */
	private void drawArrows(Graphics2D g2){
		arrow.updateDimension(this.getWidth(), this.getHeight());
		arrow.drawUp(g2, margin.x);
		arrow.drawRight(g2, margin.y);
	}
	
	/**
	 * Draws a vertical and a horizontal scale on the drawn arrows.
	 * @param g2 Graphic2D that are to be used for drawing these scales.
	 */
	private void drawScales(Graphics2D g2){
		arrow.drawHorizontalScale(g2, dd.getHorizontalScale(), dd.getHorizontalUnit());
		arrow.drawVerticalScale(g2, dd.getVerticalScale(), dd.getVerticalUnit());
	}
	
	/**
	 * Plot the input points in the diagram.
	 * The following BasicStroke are used 1.8f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND.
	 * @param g2 Graphic2D.
	 * @param points Points to be plotted.
	 */
	private void plot(Graphics2D g2, Point2D.Float[] points){
		BasicStroke old = (BasicStroke) g2.getStroke();
		final BasicStroke using = new BasicStroke(1.8f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
		g2.setStroke(using);
				
		for (int i = 0; i < points.length - 1; i++)
			g2.draw(new Line2D.Float(points[i], points[i + 1]));

		// Restore stroke
		g2.setStroke(old);
	}
	
	/**
	 * Normalize the data values for the diagram, by adjusting the values so that
	 * the largest value becomes the highest point in the diagram and other points
	 * are calculated in relation to this value.
	 * @param data Data to be normalized.
	 * @param largest Largest value in data.
	 * @return Points Ready to be plotted.
	 */
	private Point2D.Float[] normalizeData (int[] data, int largest){
		float ratio = 0; 
		float plotHeight = this.getHeight() - margin.y - arrowLength; // skipping line width.
		float step = ( this.getWidth() - margin.x - arrowLength ) / ( data.length - 1 );
		float[] converted = new float[data.length];
		for(int i=0;i<converted.length;i++)
			converted[i] = data[i];
		Point2D.Float[] normalized = new Point2D.Float[data.length];
		for(int i=0;i<converted.length;i++){
			ratio = converted[i] / largest;
			normalized[i] = new Point2D.Float(i*step+margin.x, plotHeight - plotHeight*ratio + arrowLength);
		}
		return normalized;
	}
	
	
	private static class Arrows {
		private float width, height, arrowWidth, arrowLength;
		private Point2D.Float margin = new Point2D.Float(0,0);
		private Point2D.Float lineWidth = new Point2D.Float(0,0);
		
		private Arrows(float width, float height, float arrowWidth, float arrowLength){
			this.width = width;
			this.height = height;
			this.arrowLength = arrowLength;
			this.arrowWidth = arrowWidth;
		}
		
		private void updateDimension(float width, float height){
			this.width = width;
			this.height = height;
		}
		
		private void drawRight(Graphics2D g2, float margin){
			this.margin.y=margin;
			this.lineWidth.y = ((BasicStroke) g2.getStroke()).getLineWidth();
			Point2D.Float arrowStart = new Point2D.Float(width, height-margin);
			Point2D.Float arrowEnd = new Point2D.Float(width-arrowLength, height-margin);
			
			float ctrlX = arrowStart.x - arrowLength * 0.5f;
			float ctrlY = arrowWidth * 0.2f;
			float pitch = arrowLength * 0.1f;
			
			Path2D.Float arrowHead = new Path2D.Float();
			arrowHead.moveTo( arrowEnd.x, arrowEnd.y - arrowWidth );
			arrowHead.quadTo( ctrlX, arrowEnd.y - ctrlY, arrowStart.x, arrowStart.y );
			arrowHead.quadTo( ctrlX, arrowEnd.y + ctrlY, arrowEnd.x, arrowEnd.y + arrowWidth );
			arrowHead.lineTo( arrowEnd.x + pitch, arrowEnd.y );
			arrowHead.lineTo( arrowEnd.x, arrowEnd.y - arrowWidth );
			g2.fill(arrowHead);
			
			Point2D.Float shaftStart = new Point2D.Float(0,height-margin);
			Point2D.Float shaftEnd = new Point2D.Float(arrowEnd.x+pitch,height-margin);
			g2.draw(new Line2D.Float(shaftStart, shaftEnd));
		}
		
		private void drawUp(Graphics2D g2, float margin){
			this.margin.x=margin;
			this.lineWidth.x = ((BasicStroke) g2.getStroke()).getLineWidth();
			Point2D.Float arrowStart = new Point2D.Float(margin, 0);
			Point2D.Float arrowEnd = new Point2D.Float(margin, arrowLength);
			
			float ctrlX = arrowWidth * 0.2f;
			float ctrlY = arrowStart.y + arrowLength * 0.5f;
			float pitch = arrowLength * 0.1f;
						
			Path2D.Float arrowHead = new Path2D.Float();
			arrowHead.moveTo( margin-arrowWidth, arrowLength );
			arrowHead.quadTo( margin-ctrlX, ctrlY, margin, 0 );
			arrowHead.quadTo( margin+ctrlX, ctrlY, margin+arrowWidth, arrowLength );
			arrowHead.lineTo( arrowEnd.x, arrowEnd.y - pitch );
			arrowHead.lineTo( arrowEnd.x - arrowWidth, arrowEnd.y);
			g2.fill(arrowHead);
			
			Point2D.Float shaftStart = new Point2D.Float(margin, arrowLength-pitch);
			Point2D.Float shaftEnd = new Point2D.Float(margin, height);
			g2.draw(new Line2D.Float(shaftStart, shaftEnd));
		}
		
		private void drawHorizontalScale(Graphics2D g2, String[] marks, String unit){
			Point2D.Float unitPoint = new Point2D.Float( width-unit.length()*5-arrowLength/2, height-margin.y );
			Point2D.Float start = new Point2D.Float(margin.x + lineWidth.x, 0);
			
			if( margin.y-arrowWidth>10 ){ // 10 - roughly the height of letters
				start.y =  unitPoint.y = unitPoint.y + arrowWidth * 0.5f + 15;
				float stepSize = ( width - margin.x - arrowLength - 5f) / marks.length;
				float x=0,y = height - margin.y;
				start.x+=stepSize;
				for(int i=0; i<marks.length-1 ;i++){
					x = start.x + stepSize*i;
					g2.drawString(marks[i], x-marks[i].length()*3, start.y);
					g2.draw(new Line2D.Float(x, y+arrowWidth, x, y));
				}
			}
			else
				unitPoint.y -= arrowWidth + 5;
			g2.drawString(unit, unitPoint.x, unitPoint.y);
		}
		
		private void drawVerticalScale(Graphics2D g2, String[] marks, String unit){
			Point2D.Float unitPoint = new Point2D.Float(margin.x, arrowLength * 0.7f);
			Point2D.Float start = new Point2D.Float(margin.x - lineWidth.x, height - margin.y - lineWidth.y);
			
			int length=0;
			for(int i=0;i<marks.length-1;i++)
				if(marks[i].length()>length)
					length = marks[i].length();
			if(unit.length()>length)
				length = unit.length();
			
			if( margin.x - arrowWidth - length*8>0 ){
				unitPoint.x -= unit.length()*6 + arrowWidth * 0.8f;
				float stepSize = ( height - margin.y - arrowLength - 5f) / marks.length;
				float y=0, x=0;
				start.y -= stepSize;
				for(int i=0;i<marks.length-1;i++){
					y = start.y - i*stepSize;
					x = margin.x - arrowWidth;
					g2.drawString(marks[i], x - length*8 , y+4);
					g2.draw( new Line2D.Float(x, y, margin.x, y));
				}
			} else
				unitPoint.x += arrowWidth * 0.5f;
			g2.drawString(unit, unitPoint.x, unitPoint.y);
		}
	}
}